# 一、常见的OOM异常

常见的OOM可以分为以下三类:

  1. 线程打开过多
  2. 内存不足(内存溢出或内存泄漏)
  3. 文件打开过多(IO流处理)

# 1.1 线程OOM

异常日志

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
	at java.base/java.lang.Thread.start0(Native Method)
	at java.base/java.lang.Thread.start(Thread.java:801)
	at java.base/java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:939)
	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1356)
	at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:118)
	at com.company.hpv9.test.main(test.java:11)
1
2
3
4
5
6
7

排查过程

通过OOM异常日志,可以知道是由于无法创建线程导致的OOM,并且可以根据栈信息定位问题代码的位置。

// 查看当前开启的线程数量
cat proc/{pid}/status
1
2

image-20220913004852163

优化

  1. 服务器参数优化

// 查看操作系统对每个进程的线程数限制,可以进行调大

cat /proc/sys/kernel/threads-max

线程数限制.png

  1. 业务代码优化
    • 替换new Thread为线程池,控制线程创建数量

# 1.2 文件OOM

异常信息

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
Java.lang.OutOfMemoryError: Could not allocate JNI Env
1
2

问题分析

系统问题:操作系统对每个进程都有资源的限制,而默认的资源限制通常比较少,需要我们根据业务需求进行调高。

代码问题:如果IO操作有误,没有及时关闭IO流,或者突然大量用户线程同时创建大量文件,也可能出现这个问题。

排查过程

  1. 通过异常日志,可以知道是在哪里位置出现了文件创建异常问题,初步检查该位置的代码。

  2. 如果没问题,则通过收集到的dumpFd(文件描述符),可以看到很多重复的文件名,进而大致定位到问题。

  3. 检查到异常文件名后,再检查该文件的创建位置,进而进行IO流相关代码的检查。

  • 查看文件资源限制

    • cat /proc/pid/limits

    image-20220913005902463

    • ulimit -n

    image-20220913225351404

监控机制

  • 文件描述符监控:Linux 系统一切皆文件,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面)。当fd数大于1000个,或者fd连续递增超过50个,就触发fd收集,将fd对应的文件路径上报到后台。
  • IO监控:监控完整的IO操作,包括open、read、write、close,通过在IO操作中插入勾子进行监控。

# 1.3 内存OOM

异常信息

  • 堆区
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.company.hpv9.test.main(test.java:13)
1
2
  • 元方法区
// todo
1
  • 堆外内存
nio handle failed java.lang.OutOfMemoryError: Direct buffer memory at org.eclipse.jetty.io.nio.xxxx
at org.eclipse.jetty.io.nio.xxxx at org.eclipse.jetty.io.nio.xxxx
1
2

问题分析

JVM中JMM发生OOM的区域最常见的是堆区,元方法区和堆外内存。

配置问题:

  • 堆区太小(内存溢出):JVM默认的堆区大小比较少,需要通过参数配置调大堆区内存,而堆区又分为年轻代和老年代,可以参考下面的#参数调优进行相关配置。
  • Eden区太小 + System.gc关闭(过早晋升):如果年轻代内存分配过小频繁YGC,导致引用堆外内存的对象提前进入老年代,并且老年代没来得及进行GC,则可能导致堆外内存OOM。

代码问题:

  • 堆区:代码中的错误处理导致内存泄漏,如ThreadLocal。
  • 元方法区:反射的异常使用导致类不断被加载,进而导致元方法区OOM。
  • 堆外内存:(部分框架会用到堆外内存,例如Netty)
    • 堆外内存泄漏(框架bug-美团Netty案例 (opens new window)):Netty框架源码NPE导致内存泄漏,进而导致堆外内存的OOM,其通过反射监控堆外内存的大小,并逐渐缩小范围,最后找到并处理bug。

排查过程

  1. 配置JVM参数打印OOM时的堆区信息(.hprof文件)
  2. 通过MAT工具解析hprof文件
  3. 通过Histogram视图可以查看当前的实例占用的内存情况(通常会有占用内存很大的实例对象)
  4. 通过对象引用、GC Root Path等调用链视图,定位问题代码

# 二、MAT内存监控工具

img

image-20220914002550594

# 2.1 常见内存分析工具对比

产品功能 MAT JProfiler Visual VM jhat jmap hprof
对象关联分析、深浅堆、GC ROOT、内存泄漏检测、线程分析、提供自定义程序扩展扩展 Y N N N N N
离线全局分析 Y N Y Y N N
内存实时分配情况 N Y Y Y Y Y
OQL Y N Y N N N
内存分配堆栈、热点比例 N Y N N N N
堆外内存分析 N N N N N N

# 2.2 基础概念

Shallow Heap

Shallow Heap 代表一个对象结构自身所占用的内存大小,不包括其属性引用对象所占的内存。如 java.util.ArrayList 对象的 Shallow Heap 包含8字节的对象头、8字节的对象数组属性 elementData 引用 、 4字节的 size 属性、4字节的 modCount 属性。

Retained Set

一个对象的 Retained Set,指的是该对象被 GC 回收后,所有能被回收的对象集合。

Retained Heap

Retained Heap 是一个对象被 GC 回收后,可释放的内存大小,等于释放对象的 Retained Heap 中所有对象的 Shallow Heap 的和。

Dominator tree

Dominator tree 是根据对象引用及支配关系生成的整体树状图,支配树清晰描述了对象间的依赖关系,可以认为是对象的引用树。

  • Dominator tree 中任一节点的子树就是被该节点支配的节点集合,也就是其 Retain Set。

OQL

OQL 是类似于 SQL 的 MAT 专用统一查询语言,可以根据复杂的查询条件对 dump 文件中的类或者对象等数据进行查询筛选。

references

outgoing references、incoming references 可以直击对象间依赖关系,MAT 也提供了链式快速操作。

  • outgoing references:对象引用的外部对象(注意不包含对象的基本类型属性。基本属性内容可在 inspector 查看)。
  • incoming references:直接引用了当前对象的对象,每个对象的 incoming references 可能有 0 到多个。

# 2.3 MAT功能

MAT 的工作原理是对 dump 文件建立多种索引,并基于索引来实现以下四个功能,分别为【内存分布、对象依赖、对象状态和条件检索】,并通过可视化展现辅助 Developer 精细化了解 JVM 堆内存全貌。

  • [1]内存分布(重点)

    • Overview(全局概览信息):堆内存大小、对象个数、类的个数、类加载器的个数、GC root 个数等基本信息。

    image-20220914001200494

    • Dominator tree(对象引用树):包含对象的内存占用大小及对象的相互引用情况。按对象的 Retain Heap 排序,也支持按多个维度聚类统计。

    image-20220914001214948

    • Histogram(实例内存占比):罗列每个类实例的内存占比,包括自身内存占用量(Shallow Heap)及支配对象的内存占用量(Retain Heap),支持按 package、class loader、super class、class 聚类统计。

    image-20220914001229614

    • Leak Suspects(可疑泄漏点):MAT提供的问题分析工具,可以简单分析可能出现问题的地方,可解决一些基础问题,但复杂的问题往往帮助有限。

    image-20220914001517072

  • [2]对象间依赖(如实体对象引用关系、线程引用关系、GC Root引用关系等)

    • Thread overview(线程调用栈):展现转储 dump 文件时线程栈帧等详细状态,也提供各线程的 Retain Heap 等关联内存信息。(两种方式查看,一种是直接点击上方的Thread overview,查看所有线程信息,一种是在Dominator tree中右键查看其线程信息)

    image-20220914002411304

    image-20220914001714918

    image-20220914002038575

    • Dominator tree:该视图即为对象间的引用关系。除了直接用Dominator视图之外(显示了线程或class loader的对象引用关系),还可以在Histogram视图右键如下图,显示该对象及引用了该对象的对象。

    image-20220915001044459

    • Path To GC Roots(对象到GC Root的引用链路):提供任一对象到GC Root的链路详情,帮助了解不能被 GC 回收的原因

    image-20220914235837087

    image-20220915000013601

  • [3]对象状态(内存占用量、字段属性值等)

    • inspector面板:可以查看对象的基本信息,包括类名、所在包、父类、类加载器、对象占用内存、GC root信息。下面的Statics可以查看其基本数据类型,对象右键outcoming视图可以查看其调用的外部对象。

    image-20220915002441866

    • (TODO)集合状态的检测,如:通过 ArrayList 或数组的填充率定位空集合空数组造成的内存浪费、通过 HashMap 冲突率判定 hash 策略是否合理等。
  • [4]条件检索(OQL、正则匹配查询等)

    • 按虚拟内存地址寻址:根据对象的十六进制地址查找对象。
    • OQL:提供一种类似于SQL的对象(类)级别统一结构化查询语言。如:查找 size=0 且未使用过的 ArrayList: select * from java.util.ArrayList where size=0 and modCount=0;查找所有的String的length属性的: select s.length from instanceof String s。

image-20220913003713039

# 三、项目实践

# 3.1 OOM异常的排查流程

  1. 日志:通过观察日志,再结合最近的上线版本或者更改的代码看能否快速定位到问题。
  2. Heap Dump:如果无法直接通过日志分析出,则利用内存分析工具(例如MAT)分析Heap Dump文件,通过其内存视图检查是内存溢出还是内存泄漏问题。
  3. 对象引用关系链:如果是内存泄漏问题,则通过其线程调用链视图、对象引用视图、GC Root链路定位出现内存泄漏的代码区域,并进行修复。
  4. 参数优化:如果不是内存泄漏问题,即内存中存在的实例都是合理的,那么就需要考虑对内存进行调优,可以从下面几个角度考虑:
    • 如果当前物理内存还未分配满,则考虑调大堆区内存大小:-Xms,-Xmx
    • 如果物理内存已满,则考虑加大内存或者部署多实例进行负载均衡

# 3.2 项目遇到的OOM

OOM异常:

  1. 情报板自动发布大量调用了new Thread,导致无法创建新的线程

解决:

  • 通过日志即+最近上线的代码即快速定位到问题代码处
  • 该用线程池解决问题
  1. 解析大XML导致的OOM

TODO

原因:BW源码解析XML存在bug导致了内存泄漏

解决:

  • Heap Dump when OOM
  • 通过MAT解析堆快照
    • 通过实例内存视图定位大内存实例
    • 通过对象引用视图定位问题代码
  • 引用第三方框架进行XML解析

No Buffer space available

  1. 大量创建socket连接导致无法创建新的连接(十多万)

原因:与设备通信过程通过TCP进行连接,而每次通信都创建了新的连接,旧的连接又没有释放

解决:

  • 日志分析定位问题
  • 建立缓存连接管道解决

# 四、参数调优

# 4.1 JVM参数分类

例子:-verbose:class,-verbose:gc,-verbose:jni……

  • 非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;

例子:Xms20m,-Xmx20m,-Xmn20m,-Xss128k……

  • 非Stable参数(-XX):此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;

例子:-XX:+PrintGCDetails,-XX:-UseParallelGC,-XX:+PrintGCTimeStamps……

# 4.2 常见参数调优

结合个人面试准备中的面试经历模块

内存相关(重点)

对于堆内存大小的调整,JDK官方给了下面的建议进行调整:

  1. 堆大小(-Xms,-Xmx):老年代存活对象的3-4倍,即【Full GC之后的剩余的老年代内存占用】的3-4倍。
  2. 元空间大小(-XX:MetaspaceSize,-XX:MaxMetaspaceSize):老年代存活对象的1.2-1.5倍。
  3. 年轻代大小(-Xmn):老年代存活对象的1-1.5倍。
  4. 老年代大小:老年代存活对象的2-3倍。

img

  • 辅助参数:

    • -XX:+HeapDumpOnOutOfMemoryError:当抛出OOM时进行HeapDump
    • -XX:HeapDumpPath=/home/tmp/logs
  • 大小调整:

    • -Xms20m:设置jvm初始化堆大小为20m,一般与-Xmx相同,避免动态扩容带来的频繁GC
    • -Xmx20m:设置jvm最大可用内存大小为20m
    • -Xmn20m:设置新生代大小为20m
    • -Xss128k:设置每个线程的栈大小为128k
    • -XX:NewRatio:设置老年代和年轻代的比例,默认是2,即老年代:年轻代为2:1
    • -XX:SurvivorRatio=8:配置年轻代中的eden/survivor的比例为8,即如果新生代内存内存为10M,会分配给Eden-8M,Survivor(from)-1M,Survivor(to)-1M

img

GC相关

  • 垃圾回收器

    • -XX:UseSerialGC
    • -XX:+UseConcMarkSweepGC:使用CMS收集器(jdk14已经被移除)
    • -XX:+UseParallelOldGC:使用ParallelGC垃圾回收器
    • -XX:+UseG1GC:使用G1垃圾回收器

    image-20220913223539026

  • -XX:CMSInitiatingOccupancyFraction=80 CMS gc,表示在老年代达到80%使用率时马上进行回收

  • -XX:+PrintGCDetails:打印GC详情

    • eden space 8192K, 33% used:eden区域总共8192k,使用了33%。2731/8192约等于0.33
    • from space 1024K, 0% used
    • to space 1024K, 0%used

image-20220913220538466image-20220913220152081

  • -XX:+PrintGCTimeStamps:打印时间戳

# 四、参考

Last Updated: 2023-2-9 4:55:04 ├F10: PM┤